// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE

(function(mod) {
  if (typeof exports == 'object' && typeof module == 'object')
    // CommonJS
    mod(require('../../lib/codemirror'));
  else if (typeof define == 'function' && define.amd)
    // AMD
    define(['../../lib/codemirror'], mod);
  // Plain browser env
  else mod(CodeMirror);
})(function(CodeMirror) {
  'use strict';

  CodeMirror.defineMode('xquery', function() {
    // The keywords object is set to the result of this self executing
    // function. Each keyword is a property of the keywords object whose
    // value is {type: atype, style: astyle}
    var keywords = (function() {
      // convenience functions used to build keywords object
      function kw(type) {
        return { type: type, style: 'keyword' };
      }
      var operator = kw('operator'),
        atom = { type: 'atom', style: 'atom' },
        punctuation = { type: 'punctuation', style: null },
        qualifier = { type: 'axis_specifier', style: 'qualifier' };

      // kwObj is what is return from this function at the end
      var kwObj = {
        ',': punctuation,
      };

      // a list of 'basic' keywords. For each add a property to kwObj with the value of
      // {type: basic[i], style: "keyword"} e.g. 'after' --> {type: "after", style: "keyword"}
      var basic = [
        'after',
        'all',
        'allowing',
        'ancestor',
        'ancestor-or-self',
        'any',
        'array',
        'as',
        'ascending',
        'at',
        'attribute',
        'base-uri',
        'before',
        'boundary-space',
        'by',
        'case',
        'cast',
        'castable',
        'catch',
        'child',
        'collation',
        'comment',
        'construction',
        'contains',
        'content',
        'context',
        'copy',
        'copy-namespaces',
        'count',
        'decimal-format',
        'declare',
        'default',
        'delete',
        'descendant',
        'descendant-or-self',
        'descending',
        'diacritics',
        'different',
        'distance',
        'document',
        'document-node',
        'element',
        'else',
        'empty',
        'empty-sequence',
        'encoding',
        'end',
        'entire',
        'every',
        'exactly',
        'except',
        'external',
        'first',
        'following',
        'following-sibling',
        'for',
        'from',
        'ftand',
        'ftnot',
        'ft-option',
        'ftor',
        'function',
        'fuzzy',
        'greatest',
        'group',
        'if',
        'import',
        'in',
        'inherit',
        'insensitive',
        'insert',
        'instance',
        'intersect',
        'into',
        'invoke',
        'is',
        'item',
        'language',
        'last',
        'lax',
        'least',
        'let',
        'levels',
        'lowercase',
        'map',
        'modify',
        'module',
        'most',
        'namespace',
        'next',
        'no',
        'node',
        'nodes',
        'no-inherit',
        'no-preserve',
        'not',
        'occurs',
        'of',
        'only',
        'option',
        'order',
        'ordered',
        'ordering',
        'paragraph',
        'paragraphs',
        'parent',
        'phrase',
        'preceding',
        'preceding-sibling',
        'preserve',
        'previous',
        'processing-instruction',
        'relationship',
        'rename',
        'replace',
        'return',
        'revalidation',
        'same',
        'satisfies',
        'schema',
        'schema-attribute',
        'schema-element',
        'score',
        'self',
        'sensitive',
        'sentence',
        'sentences',
        'sequence',
        'skip',
        'sliding',
        'some',
        'stable',
        'start',
        'stemming',
        'stop',
        'strict',
        'strip',
        'switch',
        'text',
        'then',
        'thesaurus',
        'times',
        'to',
        'transform',
        'treat',
        'try',
        'tumbling',
        'type',
        'typeswitch',
        'union',
        'unordered',
        'update',
        'updating',
        'uppercase',
        'using',
        'validate',
        'value',
        'variable',
        'version',
        'weight',
        'when',
        'where',
        'wildcards',
        'window',
        'with',
        'without',
        'word',
        'words',
        'xquery',
      ];
      for (var i = 0, l = basic.length; i < l; i++) {
        kwObj[basic[i]] = kw(basic[i]);
      }

      // a list of types. For each add a property to kwObj with the value of
      // {type: "atom", style: "atom"}
      var types = [
        'xs:anyAtomicType',
        'xs:anySimpleType',
        'xs:anyType',
        'xs:anyURI',
        'xs:base64Binary',
        'xs:boolean',
        'xs:byte',
        'xs:date',
        'xs:dateTime',
        'xs:dateTimeStamp',
        'xs:dayTimeDuration',
        'xs:decimal',
        'xs:double',
        'xs:duration',
        'xs:ENTITIES',
        'xs:ENTITY',
        'xs:float',
        'xs:gDay',
        'xs:gMonth',
        'xs:gMonthDay',
        'xs:gYear',
        'xs:gYearMonth',
        'xs:hexBinary',
        'xs:ID',
        'xs:IDREF',
        'xs:IDREFS',
        'xs:int',
        'xs:integer',
        'xs:item',
        'xs:java',
        'xs:language',
        'xs:long',
        'xs:Name',
        'xs:NCName',
        'xs:negativeInteger',
        'xs:NMTOKEN',
        'xs:NMTOKENS',
        'xs:nonNegativeInteger',
        'xs:nonPositiveInteger',
        'xs:normalizedString',
        'xs:NOTATION',
        'xs:numeric',
        'xs:positiveInteger',
        'xs:precisionDecimal',
        'xs:QName',
        'xs:short',
        'xs:string',
        'xs:time',
        'xs:token',
        'xs:unsignedByte',
        'xs:unsignedInt',
        'xs:unsignedLong',
        'xs:unsignedShort',
        'xs:untyped',
        'xs:untypedAtomic',
        'xs:yearMonthDuration',
      ];
      for (var i = 0, l = types.length; i < l; i++) {
        kwObj[types[i]] = atom;
      }

      // each operator will add a property to kwObj with value of {type: "operator", style: "keyword"}
      var operators = [
        'eq',
        'ne',
        'lt',
        'le',
        'gt',
        'ge',
        ':=',
        '=',
        '>',
        '>=',
        '<',
        '<=',
        '.',
        '|',
        '?',
        'and',
        'or',
        'div',
        'idiv',
        'mod',
        '*',
        '/',
        '+',
        '-',
      ];
      for (var i = 0, l = operators.length; i < l; i++) {
        kwObj[operators[i]] = operator;
      }

      // each axis_specifiers will add a property to kwObj with value of {type: "axis_specifier", style: "qualifier"}
      var axis_specifiers = [
        'self::',
        'attribute::',
        'child::',
        'descendant::',
        'descendant-or-self::',
        'parent::',
        'ancestor::',
        'ancestor-or-self::',
        'following::',
        'preceding::',
        'following-sibling::',
        'preceding-sibling::',
      ];
      for (var i = 0, l = axis_specifiers.length; i < l; i++) {
        kwObj[axis_specifiers[i]] = qualifier;
      }

      return kwObj;
    })();

    function chain(stream, state, f) {
      state.tokenize = f;
      return f(stream, state);
    }

    // the primary mode tokenizer
    function tokenBase(stream, state) {
      var ch = stream.next(),
        mightBeFunction = false,
        isEQName = isEQNameAhead(stream);

      // an XML tag (if not in some sub, chained tokenizer)
      if (ch == '<') {
        if (stream.match('!--', true))
          return chain(stream, state, tokenXMLComment);

        if (stream.match('![CDATA', false)) {
          state.tokenize = tokenCDATA;
          return 'tag';
        }

        if (stream.match('?', false)) {
          return chain(stream, state, tokenPreProcessing);
        }

        var isclose = stream.eat('/');
        stream.eatSpace();
        var tagName = '',
          c;
        while ((c = stream.eat(/[^\s\u00a0=<>\"\'\/?]/))) tagName += c;

        return chain(stream, state, tokenTag(tagName, isclose));
      }
      // start code block
      else if (ch == '{') {
        pushStateStack(state, { type: 'codeblock' });
        return null;
      }
      // end code block
      else if (ch == '}') {
        popStateStack(state);
        return null;
      }
      // if we're in an XML block
      else if (isInXmlBlock(state)) {
        if (ch == '>') return 'tag';
        else if (ch == '/' && stream.eat('>')) {
          popStateStack(state);
          return 'tag';
        } else return 'variable';
      }
      // if a number
      else if (/\d/.test(ch)) {
        stream.match(/^\d*(?:\.\d*)?(?:E[+\-]?\d+)?/);
        return 'atom';
      }
      // comment start
      else if (ch === '(' && stream.eat(':')) {
        pushStateStack(state, { type: 'comment' });
        return chain(stream, state, tokenComment);
      }
      // quoted string
      else if (!isEQName && (ch === '"' || ch === "'"))
        return chain(stream, state, tokenString(ch));
      // variable
      else if (ch === '$') {
        return chain(stream, state, tokenVariable);
      }
      // assignment
      else if (ch === ':' && stream.eat('=')) {
        return 'keyword';
      }
      // open paren
      else if (ch === '(') {
        pushStateStack(state, { type: 'paren' });
        return null;
      }
      // close paren
      else if (ch === ')') {
        popStateStack(state);
        return null;
      }
      // open paren
      else if (ch === '[') {
        pushStateStack(state, { type: 'bracket' });
        return null;
      }
      // close paren
      else if (ch === ']') {
        popStateStack(state);
        return null;
      } else {
        var known = keywords.propertyIsEnumerable(ch) && keywords[ch];

        // if there's a EQName ahead, consume the rest of the string portion, it's likely a function
        if (isEQName && ch === '"') while (stream.next() !== '"') {}
        if (isEQName && ch === "'") while (stream.next() !== "'") {}

        // gobble up a word if the character is not known
        if (!known) stream.eatWhile(/[\w\$_-]/);

        // gobble a colon in the case that is a lib func type call fn:doc
        var foundColon = stream.eat(':');

        // if there's not a second colon, gobble another word. Otherwise, it's probably an axis specifier
        // which should get matched as a keyword
        if (!stream.eat(':') && foundColon) {
          stream.eatWhile(/[\w\$_-]/);
        }
        // if the next non whitespace character is an open paren, this is probably a function (if not a keyword of other sort)
        if (stream.match(/^[ \t]*\(/, false)) {
          mightBeFunction = true;
        }
        // is the word a keyword?
        var word = stream.current();
        known = keywords.propertyIsEnumerable(word) && keywords[word];

        // if we think it's a function call but not yet known,
        // set style to variable for now for lack of something better
        if (mightBeFunction && !known)
          known = { type: 'function_call', style: 'variable def' };

        // if the previous word was element, attribute, axis specifier, this word should be the name of that
        if (isInXmlConstructor(state)) {
          popStateStack(state);
          return 'variable';
        }
        // as previously checked, if the word is element,attribute, axis specifier, call it an "xmlconstructor" and
        // push the stack so we know to look for it on the next word
        if (
          word == 'element' ||
          word == 'attribute' ||
          known.type == 'axis_specifier'
        )
          pushStateStack(state, { type: 'xmlconstructor' });

        // if the word is known, return the details of that else just call this a generic 'word'
        return known ? known.style : 'variable';
      }
    }

    // handle comments, including nested
    function tokenComment(stream, state) {
      var maybeEnd = false,
        maybeNested = false,
        nestedCount = 0,
        ch;
      while ((ch = stream.next())) {
        if (ch == ')' && maybeEnd) {
          if (nestedCount > 0) nestedCount--;
          else {
            popStateStack(state);
            break;
          }
        } else if (ch == ':' && maybeNested) {
          nestedCount++;
        }
        maybeEnd = ch == ':';
        maybeNested = ch == '(';
      }

      return 'comment';
    }

    // tokenizer for string literals
    // optionally pass a tokenizer function to set state.tokenize back to when finished
    function tokenString(quote, f) {
      return function(stream, state) {
        var ch;

        if (isInString(state) && stream.current() == quote) {
          popStateStack(state);
          if (f) state.tokenize = f;
          return 'string';
        }

        pushStateStack(state, {
          type: 'string',
          name: quote,
          tokenize: tokenString(quote, f),
        });

        // if we're in a string and in an XML block, allow an embedded code block
        if (stream.match('{', false) && isInXmlAttributeBlock(state)) {
          state.tokenize = tokenBase;
          return 'string';
        }

        while ((ch = stream.next())) {
          if (ch == quote) {
            popStateStack(state);
            if (f) state.tokenize = f;
            break;
          } else {
            // if we're in a string and in an XML block, allow an embedded code block in an attribute
            if (stream.match('{', false) && isInXmlAttributeBlock(state)) {
              state.tokenize = tokenBase;
              return 'string';
            }
          }
        }

        return 'string';
      };
    }

    // tokenizer for variables
    function tokenVariable(stream, state) {
      var isVariableChar = /[\w\$_-]/;

      // a variable may start with a quoted EQName so if the next character is quote, consume to the next quote
      if (stream.eat('"')) {
        while (stream.next() !== '"') {}
        stream.eat(':');
      } else {
        stream.eatWhile(isVariableChar);
        if (!stream.match(':=', false)) stream.eat(':');
      }
      stream.eatWhile(isVariableChar);
      state.tokenize = tokenBase;
      return 'variable';
    }

    // tokenizer for XML tags
    function tokenTag(name, isclose) {
      return function(stream, state) {
        stream.eatSpace();
        if (isclose && stream.eat('>')) {
          popStateStack(state);
          state.tokenize = tokenBase;
          return 'tag';
        }
        // self closing tag without attributes?
        if (!stream.eat('/'))
          pushStateStack(state, {
            type: 'tag',
            name: name,
            tokenize: tokenBase,
          });
        if (!stream.eat('>')) {
          state.tokenize = tokenAttribute;
          return 'tag';
        } else {
          state.tokenize = tokenBase;
        }
        return 'tag';
      };
    }

    // tokenizer for XML attributes
    function tokenAttribute(stream, state) {
      var ch = stream.next();

      if (ch == '/' && stream.eat('>')) {
        if (isInXmlAttributeBlock(state)) popStateStack(state);
        if (isInXmlBlock(state)) popStateStack(state);
        return 'tag';
      }
      if (ch == '>') {
        if (isInXmlAttributeBlock(state)) popStateStack(state);
        return 'tag';
      }
      if (ch == '=') return null;
      // quoted string
      if (ch == '"' || ch == "'")
        return chain(stream, state, tokenString(ch, tokenAttribute));

      if (!isInXmlAttributeBlock(state))
        pushStateStack(state, { type: 'attribute', tokenize: tokenAttribute });

      stream.eat(/[a-zA-Z_:]/);
      stream.eatWhile(/[-a-zA-Z0-9_:.]/);
      stream.eatSpace();

      // the case where the attribute has not value and the tag was closed
      if (stream.match('>', false) || stream.match('/', false)) {
        popStateStack(state);
        state.tokenize = tokenBase;
      }

      return 'attribute';
    }

    // handle comments, including nested
    function tokenXMLComment(stream, state) {
      var ch;
      while ((ch = stream.next())) {
        if (ch == '-' && stream.match('->', true)) {
          state.tokenize = tokenBase;
          return 'comment';
        }
      }
    }

    // handle CDATA
    function tokenCDATA(stream, state) {
      var ch;
      while ((ch = stream.next())) {
        if (ch == ']' && stream.match(']', true)) {
          state.tokenize = tokenBase;
          return 'comment';
        }
      }
    }

    // handle preprocessing instructions
    function tokenPreProcessing(stream, state) {
      var ch;
      while ((ch = stream.next())) {
        if (ch == '?' && stream.match('>', true)) {
          state.tokenize = tokenBase;
          return 'comment meta';
        }
      }
    }

    // functions to test the current context of the state
    function isInXmlBlock(state) {
      return isIn(state, 'tag');
    }
    function isInXmlAttributeBlock(state) {
      return isIn(state, 'attribute');
    }
    function isInXmlConstructor(state) {
      return isIn(state, 'xmlconstructor');
    }
    function isInString(state) {
      return isIn(state, 'string');
    }

    function isEQNameAhead(stream) {
      // assume we've already eaten a quote (")
      if (stream.current() === '"') return stream.match(/^[^\"]+\"\:/, false);
      else if (stream.current() === "'")
        return stream.match(/^[^\"]+\'\:/, false);
      else return false;
    }

    function isIn(state, type) {
      return (
        state.stack.length && state.stack[state.stack.length - 1].type == type
      );
    }

    function pushStateStack(state, newState) {
      state.stack.push(newState);
    }

    function popStateStack(state) {
      state.stack.pop();
      var reinstateTokenize =
        state.stack.length && state.stack[state.stack.length - 1].tokenize;
      state.tokenize = reinstateTokenize || tokenBase;
    }

    // the interface for the mode API
    return {
      startState: function() {
        return {
          tokenize: tokenBase,
          cc: [],
          stack: [],
        };
      },

      token: function(stream, state) {
        if (stream.eatSpace()) return null;
        var style = state.tokenize(stream, state);
        return style;
      },

      blockCommentStart: '(:',
      blockCommentEnd: ':)',
    };
  });

  CodeMirror.defineMIME('application/xquery', 'xquery');
});
